﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;

using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using Microsoft.Phone.Net.NetworkInformation;

namespace WindowsPhoneAudioStreaming.Audio.Streaming
{
    public class ShoutCastStream : MediaStreamSource
    {
        private WaveFormatExtensible _waveFormat;

        private MediaStreamDescription _audioDesc;
        private long _startPosition;
        private long _currentTimeStamp;

        // you only need sample attributes for video
        private Dictionary<MediaSampleAttributeKeys, string> _emptySampleDict = new Dictionary<MediaSampleAttributeKeys, string>();

        private MP3Frame _mp3Frame;
        private int currentFrameSize;
        private int _mp3FrameHeaderLength = 4;

        // new audio stream to get data from
        private Stream audioStream;
        private static byte[] audioBuffer = new byte[4096];
        private int audioMetadataLength;
        private string audioMetadataHeader = "";
        private int audioDataCount;
        private int audioMetaInt;
        private CircularStream audioDataStream;
        private int audioBytesRead = 0;

        int bytesPosition = 0;
        public event EventHandler StreamComplete;

        string radioServerName = "";

        /// <summary>
        /// Initializes a new instance of the Mp3MediaStreamSource class.
        /// </summary>
        /// <param name="audioStream">
        /// Stream containing Mp3 data
        /// </param>
        public ShoutCastStream(string serverName)
        {
            // store the server name
            radioServerName = serverName;

            // create a wave format
            _waveFormat = new WaveFormatExtensible();
            
            // create a 2meg circular buffer, you could make this smaller is you wish
            audioDataStream = new CircularStream(2000000);
        }

        public void Shutdown()
        {
            CallStreamComplete();
        }

        void SampleIsReady()
        {
            // Create our stream descriptor

            Dictionary<MediaSourceAttributesKeys, string> availableMediaStreams = new Dictionary<MediaSourceAttributesKeys, string>();
            Dictionary<MediaStreamAttributeKeys, string> mediaStreamAttributes = new Dictionary<MediaStreamAttributeKeys, string>();
            List<MediaStreamDescription> mediaStreamDescriptions = new List<MediaStreamDescription>();

            mediaStreamAttributes[MediaStreamAttributeKeys.CodecPrivateData] = "" + _waveFormat.ToHexString() + _mp3Frame.ToHexString();

            _audioDesc = new MediaStreamDescription(MediaStreamType.Audio, mediaStreamAttributes);
            mediaStreamDescriptions.Add(_audioDesc);

            availableMediaStreams[MediaSourceAttributesKeys.CanSeek] = "0"; //forward stream
            availableMediaStreams[MediaSourceAttributesKeys.Duration] = TimeSpan.FromMinutes(5).Ticks.ToString(CultureInfo.InvariantCulture);

            currentFrameSize = _mp3Frame.FrameSize;

            ReportOpenMediaCompleted(availableMediaStreams, mediaStreamDescriptions);
        }

        protected override void OpenMediaAsync()
        {
            // connect to server and start downloading the ICY header and audio stream
            StartStream(radioServerName);
        }

        void StartStream(string server)
        {
            // Ask for the ICY shoutcast headers and lets pretend we are Winamp for compatibility

            HttpWebRequest objRequest = (HttpWebRequest)System.Net.HttpWebRequest.Create(server);
            objRequest.Headers["Icy-MetaData"] = "1";
            objRequest.Method = "GET";
            objRequest.UserAgent = "WinampMPEG/5.09";
            objRequest.AllowReadStreamBuffering = false;

            var result = (IAsyncResult)objRequest.BeginGetResponse(ResponseCallback, objRequest);
        }

        private void ResponseCallback(IAsyncResult result)
        {
            var request = (HttpWebRequest)result.AsyncState;
            HttpWebResponse response;
            try
            {
                response = (HttpWebResponse)request.EndGetResponse(result);
            }
            catch (WebException e)
            {
                string statusCode = e.Message;
                Debug.WriteLine("\nEndGetResponse() Error \n{0}", statusCode);
                return;
            }

            Debug.WriteLine("\nThe HttpHeaders are \n{0}", response.Headers);

            int metaInt = 0; // blocksize of mp3 data
            int bitRate = 128;
            string bitRateString = "";
            string stationName = "";
            string genreName = "";
            string contentType = "Unknown Content";

            // read blocksize to find metadata block
            contentType = Convert.ToString(response.Headers["Content-Type"]);
            metaInt = Convert.ToInt32(response.Headers["icy-metaint"]);

            // get the bit rate as a string because some stations have strange formating and not just the number on its own
            // search for the first number then convert it to an int
            bitRateString = Convert.ToString(response.Headers["icy-br"]);
            string bitRateStr = Regex.Match(bitRateString, @"(?<!\.)\d+").Value;
            if (bitRateStr != "")
                bitRate = Convert.ToInt32(bitRateStr);

            stationName = Convert.ToString(response.Headers["icy-name"]);
            genreName = Convert.ToString(response.Headers["icy-genre"]);

            Stream socketStream = response.GetResponseStream();

            if ((response.Headers.Count == 0) || (contentType.Length > 0 && contentType.Contains("text/plain")))
            {
                //
                // The icy header data was not in the response header so lets read the stream and read all the icy string data ourselves
                //
                string icyHeader = "";
                byte data;
                char dataChar;
                bool done = false;
                while (!done)
                {
                    // read a byte and convert it to a character
                    data = (byte)socketStream.ReadByte();
                    dataChar = Convert.ToChar(data);

                    // have we reached the end of a line ?
                    if (dataChar.Equals('\n'))
                    {
                        // we should now have a valid string - icyHeader will be a icy string command
                        if (icyHeader.Length > 0)
                        {
                            if (icyHeader.StartsWith("icy-name:"))
                            {
                                stationName = icyHeader.Substring(9).TrimEnd('\r');
                            }
                            else if (icyHeader.StartsWith("icy-genre:"))
                            {
                                genreName = icyHeader.Substring(10).TrimEnd('\r');
                            }
                            else if (icyHeader.StartsWith("icy-br:"))
                            {
                                bitRate = short.Parse(icyHeader.Substring(7).TrimEnd('\r'));
                            }
                            else if (icyHeader.StartsWith("content-type:"))
                            {
                                contentType = icyHeader.Substring(13).TrimEnd('\r');
                            }
                            else if (icyHeader.StartsWith("icy-metaint:"))
                            {
                                metaInt = int.Parse(icyHeader.Substring(12).TrimEnd('\r'));
                            }
                            else if (icyHeader.Equals("\r"))
                                done = true;
                        }

                        // we are finished with the string, clear it for the next string that is read
                        icyHeader = "";
                    }
                    else
                    {
                        // add the next character to the string
                        icyHeader += dataChar;
                    }
                }

                Debug.WriteLine("\nAudio Stream: {0}, {1}, {2} \n", stationName, contentType, genreName);
            }

            string streamInfo = "Bit Rate: " + bitRate + "\nGenre: " + genreName + "\nContent Type: " + contentType;

            int dataCount = 0;          // byte counter
            int metadataLength = 0;     // length of metadata header

            int bytesRead = 0;

            // we need to find the first 11 bits, 0xFF followed by 0xE0    -      http://www.codeguru.com/forum/showthread.php?t=449355
            byte syncBit = 0xFF;
            byte readByteSync = 0;
            byte readByte = 0;
            bool foundSyncBit = false;
            while (!foundSyncBit)
            {
                bytesRead++;
                readByte = (byte)socketStream.ReadByte();
                if (readByte.Equals(syncBit))
                {
                    readByteSync = readByte;
                    bytesRead++;
                    readByte = (byte)socketStream.ReadByte();       // TODO: this value could be 0xFF so then need to check for 0xE0 after this
                    if ((readByte & 0xE0) == 0xE0)
                        foundSyncBit = true;
                }
            }
            byte[] mp3HeaderData = new byte[_mp3FrameHeaderLength];

            mp3HeaderData[0] = readByteSync;
            mp3HeaderData[1] = readByte;
            for (int i = 2; i < 4; i++) //first 2 bytes already read, read the remaining 2
                mp3HeaderData[i] = (byte)socketStream.ReadByte();

            // Create an MP3 framn based on the mp3 header data
            _mp3Frame = new MP3Frame(mp3HeaderData, bitRate);

            // Set up the wave format
            _waveFormat.FormatTag = 85;
            _waveFormat.Channels = (short)((_mp3Frame.Channels == MP3Frame.Channel.SingleChannel) ? 1 : 2);
            _waveFormat.SamplesPerSec = _mp3Frame.SamplingRate;
            _waveFormat.AverageBytesPerSecond = _mp3Frame.Bitrate / 8;
            _waveFormat.BlockAlign = 1;
            _waveFormat.BitsPerSample = 0;
            _waveFormat.Size = 12;

            dataCount = bytesRead + 2;        // add the bytes we just read looking for ID3 frame

            // Trigger that sample is ready to play
            SampleIsReady();

            audioStream = socketStream;
            audioMetadataLength = metadataLength;
            audioMetaInt = metaInt;
            audioDataCount = dataCount;
            audioBytesRead = 0;
        }

        protected override void GetSampleAsync(MediaStreamType mediaStreamType)
        {
            MemoryStream frameStream = new MemoryStream();
            byte[] audioBuffer = new byte[1024];
            bool gotData = false;

            const int NUMFRAMES = 10;

            while (!gotData)
            {
                // read data from the stream
                int bytes = audioStream.Read(audioBuffer, 0, audioBuffer.Length);

                // stream is empty so close it and exit
                if (bytes <= 0)
                {
                    if (audioStream != null)
                        audioStream.Close();

                    Debug.WriteLine("\nStream Closed.");
                    return;
                }

                for (int i = 0; i < bytes; i++)
                {
                    if (audioMetadataLength != 0)
                    {
                        audioMetadataHeader += Convert.ToChar(audioBuffer[i]);
                        audioMetadataLength--;
                    }
                    else
                    {
                        audioDataCount++;

                        if (audioDataCount > audioMetaInt)
                        {
                            // we have read all the mp3 data, the next meta header will be next

                            // get headerlength from lengthbyte and multiply by 16 to get correct headerlength
                            audioMetadataLength = Convert.ToInt32(audioBuffer[i]) * 16;
                            audioDataCount = 0;
                        }
                        else
                        {
                            audioDataStream.WriteByte(audioBuffer[i]);

                            audioBytesRead++;
                        }
                    }
                }

                // check if there is enough audio data in the buffer
                if ((audioBytesRead > 0) && (audioBytesRead >= bytesPosition + currentFrameSize * NUMFRAMES))
                {
                    byte[] data = new byte[currentFrameSize * NUMFRAMES];
                    for (int i = 0; i < currentFrameSize * NUMFRAMES; i++)
                        data[i] = (byte)audioDataStream.ReadByte();

                    frameStream.Write(data, 0, currentFrameSize * NUMFRAMES);

                    bytesPosition += currentFrameSize * NUMFRAMES;

                    gotData = true;
                }
            }

            // Send a sample to the media player
            MediaStreamSample msSamp = new MediaStreamSample(_audioDesc,frameStream,0,currentFrameSize * NUMFRAMES,_currentTimeStamp,_emptySampleDict);
            ReportGetSampleCompleted(msSamp);

            _currentTimeStamp += AudioDurationFromBufferSize((uint)currentFrameSize * NUMFRAMES);
        }

        /// <summary>
        /// Calculate the duration of audio based on the size of the buffer
        /// </summary>
        /// <param name="audioDataSize">the buffer size in bytes</param>
        /// <returns>The duration of that buffer</returns>
        private long AudioDurationFromBufferSize(uint audioDataSize)
        {
            if (_waveFormat.AverageBytesPerSecond == 0)
            {
                return 0;
            }

            return (long)audioDataSize * 10000000 / _waveFormat.AverageBytesPerSecond;
        }

        protected override void CloseMedia()
        {
            Debug.WriteLine("\nCloseMedia()");

            // Close the stream
            _startPosition = 0;
            _audioDesc = null;

            CallStreamComplete();
        }

        protected override void SeekAsync(long seekToTime)
        {
            ReportSeekCompleted(seekToTime);
        }

        protected override void SwitchMediaStreamAsync(MediaStreamDescription mediaStreamDescription)
        {
            throw new NotImplementedException();
        }

        protected override void GetDiagnosticAsync(MediaStreamSourceDiagnosticKind diagnosticKind)
        {
            throw new NotImplementedException();
        }

        
        protected void CallStreamComplete()
        {
            // This may throw a null reference exception - that indicates that the agent did not correctly
            // subscribe to StreamComplete so it could call NotifyComplete
            if (null != StreamComplete)
            {
                StreamComplete(this, new EventArgs());
            }
        }

    }
}
